作者:陈广 日期:2018-11-20
上一篇文章,我们使用 Fiddler 抓取了一个最简单静态页面的报文进行分析,并介绍了 HTTP GET 方法。但如果想要了解其它 HTTP 方法,使用静态页面就无法满足了,继续深入下去,就需要建立动态页面了。这一系列文章本来就是讲 .NET Core 的,所以本文将使用 .NET Core 来创建动态页面以继续学习 HTTP 协议。
早期的网页都是静态的,也就是说任何人在任何地方,打开一个页面,显示的内容都是一成不变的。后面出现了新的需求,最简单的例子,不同的用户登录同一个页面,需要显示不同的内容;早上登录希望显示早上好,下午登录希望显示下午好,这此都是静态页面无法做到的,所以就出现了动态网页。两者的区别画张图就清楚了。
首先进静态网页的请求过程:
接下来是动态网页的请求过程:
可以看到,在静态网页请求中,HTML 代码是写死在文件中的,使用时传给客户端。而在动态网页申请中,Web 服务器的角色发生了转变,它成为了代理,负责将请求转发给 Dotnet 服务程序,由服务程序对请求进行处理,并根据请求实时生成 HTML 传回给 Web 服务器。
接下来我们写一个 ASP.NET Core 应用程序,以观察动态网页所传送的报文。
本例使用 .NET Coer 2.1 编写。新建一个名为 Demo 的文件夹,在文件夹上鼠标右键菜单选择【Open with Code】,使用 Visual Studio Code 打开此文件夹(在安装 vscode 结束时弹出的窗口上需要选择所有项,此项才会出现在右键菜单上)。
在 vscode 中按【Ctrl + ~】快捷键打开终端(应为波浪号下面那个符号,由于 Markdown 里此符号有别用,无法正常显示,故用波浪号代替)。输入dotnet new empty
新建一个空 web 应用程序。
首先关掉 SSL,先不使用 https。打开 Properties 文件夹下的 launchSettings.json 文件。将iisSettings
下的sslPort
项的值改为0
。然后删除demo
下的applicationUrl
项中的 https 项。最终代码如下所示(注意端口号可能有所不同):
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:19077",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"demo": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
更改 Startup.cs 代码如下:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace demo
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvcWithDefaultRoute();
}
}
}
接下来,新建一个 Controllers 文件夹,并向其添加了个名为 HomeController.cs 的文件,输入如下代码:
using Microsoft.AspNetCore.Mvc;
namespace demo.Controllers
{
public class HomeController : Controller
{
public string Index()
{
return "Hello World!";
}
}
}
此控制器非常简单,仅返回一个字符串,其实和静态网页没啥两样。
本来还是想用 Fiddler 来抓包的,但很遗憾,vscode 和 vs 自带的 IIS 都是仅供本地使用的简易 web 服务器,无法抓包,也懒得想办法解决了。突然想起来 浏览器也可以查看报文。所以干脆以后就使用浏览器来查看报文吧,方便很多,不会象 Fiddler 一样会受到大量垃圾连接的干扰。这段双 11,电脑每时每刻都会有大量垃圾弹窗,痛苦!
保存完所有文件后,选择 vscode 的【Debug】菜单中的【Start Without Debugging】,此时启动浏览器,并显示Hello World!
字样。我使用的是 Edge 浏览器,以后以此浏览器为例进行讲解。接下来在浏览器中按【F12】打开开发者工具,切换到【网络】选项卡,点击【http://localhost:5000】项查看报文,如下图所示:
在 Edge 中,首部被称为标头,你记住它是 header 就可以了,我在文章中一般都使用英文。在浏览器中无法查看报文的完整原文,都是分好类以方便查看的。可以在【http://localhost:5000】上单击鼠标右键,选择不同的项以将原文复制至剪贴板。
接下来点击【正文】选项卡,可以查看响应 Body(Edge 将主体称为正文,你记住它是 Body 就行了)。如下图所示:
控制器的基类Controller
中有一个 Request 属性,可用于读取请求报文,下表是它的常用属性:
名称 | 描述 |
---|---|
Path | 此属性返回请求 URL 的路径部分 |
QueryString | 此属性返回请求 URL 的查询字符串部分 |
Headers | 此属性返回请求 header 的字典,按名称索引 |
Body | 此属性返回可用于读取请求 body 的流 |
Form | 此属性返回按名称索引的请求中表单数据的字典 |
Cookies | 此属性返回按名称索引的请求 cookies 的字典 |
下面我们尝试读取请求报文中的 Header。更改 HomeController.cs 文件代码如下:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using System.Collections.Generic;
namespace demo.Controllers
{
public class HomeController : Controller
{
public string Index()
{
string result = "";
KeyValuePair<string, StringValues>[] headers = new KeyValuePair<string, StringValues>[Request.Headers.Count];
Request.Headers.CopyTo(headers, 0);
//用于读取Headers中的每个键值对
foreach (KeyValuePair<string, StringValues> pair in headers)
{
result += pair.Key + ":";
//每个值有可能包含多个字符串,以下是循环读取
foreach (string s in pair.Value.ToArray())
{
result += pair.Value[0] + "——";
}
result += "\r\n";
}
return result;
}
}
}
代码比较复杂,Request.Headers
属性用于读取报文中的 header,它由一个键值对集合组成,而此集合中的每个项中的值又是一个集合,这是因为同一的 header 可能有多个值。所以读起来比较麻烦。
运行程序,浏览器中显示如下结果:
Connection:Keep-Alive——
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8——
Accept-Encoding:gzip, deflate——
Accept-Language:zh-CN——
Host:localhost:5000——
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134——
Upgrade-Insecure-Requests:1——
由结果可观察到,所有的值都只有一个元素,所以只需读取字符数组中的第一个值即可。另外,如果使用 LINQ,代码会简单一些。将 HomeController.cs 代码改为:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using System.Collections.Generic;
using System.Linq;
namespace demo.Controllers
{
public class HomeController : Controller
{
public string Index()
{
string result = "";
KeyValuePair<string, StringValues>[] headers = Request.Headers.ToArray();
foreach (KeyValuePair<string, StringValues> pair in headers)
{
result += pair.Key + ":";
result += pair.Value[0] + "\r\n";
}
return result;
}
}
}
运行结果为:
Connection:Keep-Alive
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding:gzip, deflate
Accept-Language:zh-CN
Host:localhost:5000
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134
Upgrade-Insecure-Requests:1
在 ASP.NET Core 中可以通过Controller
基类的Response
属性来手动的编写响应报文并返回。更改 HomeController 代码如下:
using Microsoft.AspNetCore.Mvc;
using System.Text;
namespace demo.Controllers
{
public class HomeController : Controller
{
public void Index()
{
Response.StatusCode = 299; //指定状态码
Response.ContentType = "text/html";
//添加一个 header
string[] values={"item 1","item 2"};
Response.Headers.Add("Welcome",values);
//指定主体
byte[] content = Encoding.ASCII.GetBytes(
$"<center><h1>Hello World!</h1></center>");
Response.Body.WriteAsync(content, 0, content.Length);//返回响应
}
}
}
运行程序,返回结果如下图所示:
从图中可以看到,几项我手动更改的地方都使用红框圈了出来。上述代码指定了一个不存在的状态码,没有使用的 header,而且此 header 还有多个值。下图是响应 Body 的内容:
这样写代码是非常糟糕的,写这个例子主要是演示 ASP.NET Core 在幕后是如何工作的,实际开发中千万不要这样使用。
浏览器要向服务器传送数据,无非是通过请求报文进行传送,请求报文分为三个部分,所以数据可以放在这三个地方发送给服务器。
请求报文的第一个部分就是 URL,简单数据可以直接通过 URL 进行传送。一般情况下是使用 URL 中的查询字符串向服务器传送数据,但现在更多时候是将数据放在 URL 段中。
以下 URL 地址中:
http://www.iotxfd.cn/home/index
其中/home/index
为路径(path),路径中的home
和index
则为 URL 中的两个段。一般情况下,第一个段home
指向控制器,第二个段index
则指向 action 方法。如果要传递数据,则应当使用更多的段。假设有如下 URL:
http://localhost:5000/home/index/valueOne/valueTwo
下面我们演示如何在 action 中获取valueOne
和valueTwo
两个值。首先要更改路由,加上两个自定义段变量,更改 Startup.cs 代码如下:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace demo
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc(routes=>
{
routes.MapRoute
(
name:"default",
template:"{controller}/{action}/{v1}/{v2}"
);
});
}
}
}
这里,我们不再使用 MVC 的默认路由,而是自定义了一个路由。其中name
参数指定路由的名称,template
参数用于定义路由模式。在此路由中我们加入了两个自定义段变量v1
和v2
,用于通过在 URL 中加入额外的段向服务器传递数据。接下来更改 action 方法,以接收这两个段变量。
接下来我们将 HomeController.cs 文件更改如下:
using Microsoft.AspNetCore.Mvc;
namespace demo.Controllers
{
public class HomeController : Controller
{
public string Index(string v1,string v2)
{
return $"v1={v1}, v2={v2}";
}
}
}
在 Home 控制器中,我们给Index
action 方法添加了两个参数v1
和v2
,对应于路由模板中的v1
和v2
段变量,此时,URL 中的第三和第四个段的值就会自动传给形参v1
和v2
。运行程序,此时默认 URL 会返回一个 404 错误,我们在浏览器中输入如下 URL:
http://localhost:5000/home/index/valueOne/valueTwo
按回车后,浏览器中显示:
v1=valueOne, v2=valueTwo
两个值正好对应于我们在 URL 中输入的两个额外段。可以尝试更改 URL 中的第三和第四个段的值,然后按回车,查看浏览器显示内容的更改。
早期的程序更倾向于使用查询字符串来向服务器发送数据,上述两个额外的段如果改用查询字符串,则应更改如下:
http://localhost:5000/home/index?v1=valueOne&v2=valueTwo
?
号表示后面的是查询字符串,多个查询字符串之间使用&
分隔。下面我们将路由改为 MVC 的默认路由,更改 Startup.cs 文件如下:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace demo
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvcWithDefaultRoute();
}
}
}
Home 控制器代码无需更改,运行程序,浏览器启动时自动使用默认 URL,结果如下:
v1=, v2=
此时 URL 中并未使用查询字符串,所以v1
和v2
的值为空。更改 URL 为:
http://localhost:5000/home/index?v1=valueOne&v2=valueTwo
然后按回车键,浏览器显示如下:
v1=valueOne, v2=valueTwo
可以看到,查询字符串中的两个值已经读取出来。
一般情况下,不会通过 Header 来传送数据。浏览器通过 Header 向服务器传送用户数据一般情况下都是用在 Cookie 上。Cookie 我会在后面专门讲。但既然讲到这里,不妨演示下也是不错的选择。
我能查到的可以修改请求 Header 的方法只有一个,就是通过 JavaScript 中的XMLHttpRequest
。而XMLHttpRequest
是实现 Ajax 的核心对象。早期的 web 应用程序由于 HTTP 的无状态等原因,每次去服务器取数据时,连同整个页面的 HTML 等所有内容都要下载回本地,这实际上是浪费带宽资源,之后出现了 Ajax ,允许在不刷新页面的情况下下载数据,从而使浏览器的行为模式变得跟桌面应用程序更加类似。之后出现了 Vue、Angular、React 等 JavaScript 框架, 使得浏览器跟服务器之间只在第一次请求时传递标记需要传递 HTML,其它时候只需传递数据。它们所对应的服务器端则是 Web API。接下来我们演示如何使用 JavaScript 通过 Header 向服务器传递数据。
首先将 Index.cshtml 文件的代码更改如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<button id="Button" onclick="Btn_Click()">发送</button>
<div id="textBox"></div>
</body>
<script type="text/javascript">
function Btn_Click(){
var xhr = new XMLHttpRequest()
var url = 'http://localhost:5000/home/header';
xhr.open('GET',url);
//设置 Header
xhr.setRequestHeader("MyCustomerKey","this is my customer value");
xhr.send(); //发送 Ajax 请求
//当请求有数据返回时所调用的回调方法
xhr.onreadystatechange=function(){
var txtDiv = document.getElementById("textBox");
if (xhr.readyState == 4 && xhr.status == 200) {
//将返回的数据添加到 div 中
txtDiv.innerText = xhr.responseText;
}else{
//请求失败的处理
txtDiv.innerText = "Fail";
}
}
}
</script>
</html>
这里我在 Index 页面中安排了一个按钮,当点击按钮时,向服务器发送一个 Ajax 请求。此请求的响应使用异步实现,当有响应时会自动调用onreadystatechange
属性所指定的方法。我在页面中安排了一个 div,用于显示从服务器中返回的数据。其中设置 Header 是通过XMLHttpRequest
的setRequestHeader
方法实现的。
接下来在服务器端提控制器中添加一个针对 Ajax 请求的 action。更改 HomeController.cs 代码如下:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using System.Collections.Generic;
using System.Linq;
namespace demo.Controllers
{
public class HomeController : Controller
{
public ViewResult Index()
{
return View();
}
public string Header()
{
string result = "";
KeyValuePair<string, StringValues>[] headers = Request.Headers.ToArray();
//读取请求 header,并将所有 header 加入到一个字符串内
foreach (KeyValuePair<string, StringValues> pair in headers)
{
result += pair.Key + ":";
result += pair.Value[0] + "\r\n";
}
//返回由请求 header 拼揍成的字符串
return result;
}
}
}
这里我们声明了一个Header
action 方法,它对应的是 Index.cshtml 文件中的 Ajax 请求的 URL:
http://localhost:5000/home/header
里面的代码很熟悉,借用了前面读取请求 header 的代码。最后 action 只是简单地向浏览器返回一个字符串。
最后,运行程序,在浏览器中按【F12】打开开发者工具,然后点击【发送按钮】,在开发者工具中选择【header】项,最终显示内容如下图所示:
查看请求 header,可以看到我们添加的自定义 Header。浏览器返回的内容中也可以看到自定义 Header 项。点击开发者工具的【正文】选项卡,可以发现,数据是通过 body 传递回浏览器的。
之前我们使用的都是 GET 方法传递浏览器数据,如果需要使用 Body 进行数据传递,就需要使用 POST 方法了。这也是 GET 和 POST 方法的本质区别。浏览器需要通过表单来提交 POST 请求。
为演示 POST,我们将 Index.cshtml 代码更改如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<form action="/Home/Create" method="POST">
<input type="text" name="v1"/>
<input type="text" name="v2"/>
<input type="submit" value="提交"/>
</form>
</body>
</html>
此页面只是简单地安排了两个input
标签,用于输入传送到服务器的数据。form
标签的action
方法中我们指定了Create
action,接下来更改 HomeController.cs 文件,以添加此 action。
using Microsoft.AspNetCore.Mvc;
namespace demo.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[HttpPost]
public string Create(string v1, string v2)
{
return $"v1={v1}\r\nv2={v2}";
}
}
}
运行程序,在自动打开的浏览器中按【F12】打开开发者工具,在浏览器显示的页面的文本框中分别输入两段文字,单击【提交】按钮,可以看到服务器把我们输入的文字返回给浏览器显示。如下图所示:
选择浏览器【开发者工具】中的使用【POST】方法的【Create】项,选择右边空格的【正文】和【请求正文】。可以看到,POST 请求的数据是通过 Body 进行传送的。
本文主要介绍了浏览器与 ASP.NET Core 服务器之间的的数据传送方式以及读取方式。所使用都是非正规程序,但对于初学者来说,程序简单些,更有助于理解问题本质。要写正规代码,请参考我翻译的自由男大作《Pro ASP.NET Core MVC 2(第7版)》。
;